#pragma once

#include <stx/core/actor.hpp>

inline namespace stx
{
enum class IpType
{
    K_A,
    K_AAAA,
    K_ALL
};

using Ips = std::deque<std::string>;
[[nodiscard]] Future<Ips> queryDns(StringView const sv_domain_name,
                                   IpType const     ip_type);

DEFINE_SAFE_BOOL(SbUpdateCache);

class DnsCache final
{
    struct Key_ final
    {
        IpType      ip_type;
        std::string domain_name;

        [[nodiscard]] constexpr auto
        operator<=>(Key_ const& other) const noexcept
        {
            auto const ip_type1 = ton(this->ip_type);
            auto const ip_type2 = ton(other.ip_type);

            if (ip_type1 < ip_type2)
            {
                return std::strong_ordering::less;
            }
            else if (ip_type1 > ip_type2)
            {
                return std::strong_ordering::greater;
            }

            return (this->domain_name | views::transform(::tolower)) <=>
                   (other.domain_name | views::transform(::tolower));
        }

        [[nodiscard]] constexpr bool
        operator==(Key_ const& other) const noexcept
        {
            return 0 == (*this <=> other);
        }
    };

    using AtomicDict_ = Atomic<Map<Key_, Ips>>;
    AtomicDict_ atomic_dict_;

private:
    DnsCache() noexcept = default;

public:
    [[nodiscard]] static DnsCache& instance() noexcept;
    [[nodiscard]] Future<Ips>
    query(StringView const    sv_domain_name,
          IpType const        ip_type         = IpType::K_ALL,
          SbUpdateCache const sb_update_cache = SbUpdateCache(false));
};

DEFINE_SAFE_NUMBER(Port, std::uint16_t, 0);

class Ip4 final : public ByteArray<4>
{
    using Base_ = ByteArray<4>;

public:
    using Base_::Base_;
};

class Ip6 final : public ByteArray<16>
{
    using Base_ = ByteArray<16>;

public:
    using Base_::Base_;
};

// IP6 format: x:x:x:x:x:x:x:x
[[nodiscard]] std::optional<Ip4> getIp4(char const* const sz_ip4);
[[nodiscard]] std::optional<Ip6> getIp6(char const* const sz_ip6);

[[nodiscard]] Ip4 operator""_ip4(char const* const sz, std::size_t const size);
[[nodiscard]] Ip6 operator""_ip6(char const* const sz, std::size_t const size);

class Endpoint final
{
    union SocketAddr_
    {
        ::sockaddr     skt_addr;
        ::sockaddr_in  skt_addr4;
        ::sockaddr_in6 skt_addr6;
    };

    SocketAddr_ skt_addr_ = {};

public:
    constexpr Endpoint() noexcept = default;

    explicit Endpoint(::sockaddr const& skt_addr);
    explicit Endpoint(::sockaddr_in const& skt_addr4) noexcept;
    explicit Endpoint(::sockaddr_in6 const& skt_addr6) noexcept;

    Endpoint(Ip4 const ip4, Port const port) noexcept;
    Endpoint(Ip6 const& ip6, Port const port) noexcept;

    [[nodiscard]] Ip4 ip4() const;
    [[nodiscard]] Ip6 ip6() const;

    [[nodiscard]] explicit constexpr operator bool() const noexcept
    {
        return EXPR(this->port() > 0) && (this->isIp4() || this->isIp6());
    }

    [[nodiscard]] constexpr bool isIp4() const noexcept
    {
        return AF_INET == this->skt_addr_.skt_addr.sa_family;
    }

    [[nodiscard]] constexpr bool isIp6() const noexcept
    {
        return AF_INET6 == this->skt_addr_.skt_addr.sa_family;
    }

    [[nodiscard]] constexpr Port port() const noexcept
    {
        if (AF_INET == this->skt_addr_.skt_addr.sa_family)
        {
            return Port(::ntohs(this->skt_addr_.skt_addr4.sin_port));
        }
        else if (AF_INET6 == this->skt_addr_.skt_addr.sa_family)
        {
            return Port(::ntohs(this->skt_addr_.skt_addr6.sin6_port));
        }

        return {};
    }

    [[nodiscard]] constexpr ::sockaddr* getSockAddr() const noexcept
    {
        return std::bit_cast<::sockaddr*>(&this->skt_addr_.skt_addr);
    }

    [[nodiscard]] constexpr ::socklen_t getSockLen() const noexcept
    {
        if (this->isIp4())
        {
            return Number(sizeof(::sockaddr_in));
        }
        else if (this->isIp6())
        {
            return Number(sizeof(::sockaddr_in6));
        }

        return {};
    }
};

using RemoteEndpoint = Endpoint;
using LocalEndpoint  = Endpoint;

// s_addr may be "baidu.com" or "1.2.3.4"
[[nodiscard]] Future<Endpoint> getEndpoint(std::string const s_addr,
                                           Port const        port);

// sv_ep may be "baidu.com:8080" or "1.2.3.4:8080"
[[nodiscard]] Future<Endpoint> getEndpoint(std::string_view const sv_ep);

[[nodiscard]] std::string tos(Port const port) noexcept;
[[nodiscard]] std::string tos(Ip4 const ip4);
[[nodiscard]] std::string tos(Ip6 const& ip6);
[[nodiscard]] std::string tos(Endpoint const& ep);

[[nodiscard]] std::string getIpText(::sockaddr const& skt_addr);
[[nodiscard]] std::string getPortText(::sockaddr const& skt_addr);

struct IncomingMsg final
{
    MemSpan  ms;
    Endpoint remote_ep;
};

struct OutgoingMsg final
{
    MemView  mv;
    Endpoint remote_ep;
};

template<typename T>
concept IsOutgoingMsgRange = IsRange<T> && requires
{
    requires IsIdentical<RangeValueType<T>, OutgoingMsg>;
};

struct RecvMsgResult final
{
    int         status = {};
    IncomingMsg msg;
};

struct RecvMsgsResult final
{
    int                      status     = {};
    std::size_t              total_size = {};
    std::vector<IncomingMsg> msgs;
};

DEFINE_SAFE_BOOL(SbReusePort);

class Socket : virtual public FileDescriptor
{
public:
    using FileDescriptor::FileDescriptor;

    // IPPROTO_IP   == SOL_IP     == 0
    // IPPROTO_ICMP == SOL_SOCKET == 1
    // IPPROTO_TCP  == SOL_TCP    == 6
    // IPPROTO_UDP  == SOL_UDP    == 17

    // SO_LINGER
    // SO_REUSEADDR
    // SO_REUSEPORT
    // SO_SNDBUF
    // SO_RCVBUF
    // SO_RCVTIMEO
    // SO_SNDTIMEO
    // SO_RCVLOWAT
    // SO_SNDLOWAT
    // TCP_CORK
    // TCP_NODELAY
    // TCP_INFO
    // UDP_CORK
    // UDP_SEGMENT
    // UDP_GRO

    bool resizeRecvBuf(std::size_t const new_size) noexcept;
    bool resizeSendBuf(std::size_t const new_size) noexcept;
    bool bind(Endpoint const& local_ep) noexcept;

    [[nodiscard]] Future<Status> connect(Endpoint const& remote_ep) noexcept;

protected:
    [[nodiscard]] bool initServer_(Endpoint const&   local_ep,
                                   SbReusePort const sb_reuse_port) noexcept;
};

class UdpSocket : public Socket
{
public:
    using Socket::Socket;

    [[nodiscard]] Future<RecvMsgResult> recv(MemSpan const  ms,
                                             Duration const dur_timeout = 0ms);

    [[nodiscard]] Future<RecvMsgsResult> recv(std::vector<MemSpan> const mss,
                                              Duration const dur_timeout = 0ms);

    // The udp socket must be connected beforehand.
    [[nodiscard]] Future<std::ssize_t> send(MemView const  mv,
                                            Duration const dur_timeout = 0ms);

    // The udp socket doesn't need to be connected beforehand.
    [[nodiscard]] Future<std::ssize_t>
    send(IsOutgoingMsgRange auto const outgoing_msgs,
         Duration const                dur_timeout = 0ms);

    // The udp socket doesn't need to be connected beforehand.
    [[nodiscard]] Future<std::ssize_t> send(OutgoingMsg    outgoing_msg,
                                            Duration const dur_timeout = 0ms);
};

class TcpSocket
    : public Socket
    , public BiPipe
{
public:
    using Socket::Socket;
    using BiPipe::BiPipe;

    bool disableTimeWait() noexcept;
    bool enableNoDealy() noexcept;
    bool enableCork() noexcept;
};

DEFINE_SAFE_NUMBER(TcpBacklogSize, std::size_t, C_PAGE_SIZE);

class TcpServer final : public TcpSocket
{
public:
    using TcpSocket::TcpSocket;
    explicit TcpServer(Endpoint             local_ep,
                       TcpBacklogSize const backlog_size  = {},
                       SbReusePort const    sb_reuse_port = {});

    DEFINE_MOVE_SEMANTICS(TcpServer, {
        this->close();
        this->fd_ = other.fd_;
        other.fd_ = C_UNDEFINED;
    });

    [[nodiscard]] Future<TcpSocket>
    accept(Endpoint* const p_remote_ep = {}) noexcept;
};

class UdpServer final : public UdpSocket
{
public:
    using UdpSocket::UdpSocket;
    explicit UdpServer(Endpoint const&   local_ep,
                       SbReusePort const sb_reuse_port = {});
};

using FnOnTcpNewConn =
    std::function<Future<>(TcpSocket, RemoteEndpoint, LocalEndpoint)>;

Future<> startTcpServer(Endpoint const       local_ep,
                        FnOnTcpNewConn       fn_on_new_conn,
                        TcpBacklogSize const backlog_size,
                        SbReusePort const    sb_reuse_port);

Future<> startTcpServer(Endpoint const local_ep, FnOnTcpNewConn fn_on_new_conn);

[[nodiscard]] Future<TcpSocket> startTcpClient(Endpoint const remote_ep);
[[nodiscard]] Future<TcpSocket> startTcpClient(std::string_view const sv_addr,
                                               Port const             port);
[[nodiscard]] Future<TcpSocket> startTcpClient(std::string_view const sv_ep);

[[nodiscard]] Future<UdpSocket> startUdpClient(Endpoint const remote_ep);
} // namespace stx